package com.titanrise.crm.environment.menu;

import com.titanrise.crm.common.enums.LogEffect;
import com.titanrise.crm.common.utils.Console;
import com.titanrise.crm.common.utils.Either;
import com.titanrise.crm.common.utils.Pair;
import com.titanrise.crm.common.utils.ProjectUtil;
import com.titanrise.crm.environment.annotation.Controller;
import com.titanrise.crm.environment.annotation.FunctionConfig;
import com.titanrise.crm.environment.annotation.MenuConfig;
import com.titanrise.crm.environment.menu.action.Action;
import com.titanrise.crm.environment.menu.action.Exit;
import com.titanrise.crm.environment.menu.action.JumpToFunction;
import com.titanrise.crm.environment.menu.action.JumpToMenu;
import com.titanrise.crm.exception.RetryFunctionException;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;

public class MenuAdapter {
    private final HashMap<String, HashMap<Integer, Pair<Object, Method>>> fnMap = new HashMap<>();
    private final HashMap<String, Pair<Object, Method>> mnMap = new HashMap<>();
    public static final String divider = "=======================";
    public static final Scanner scanner = new Scanner(System.in);

    private String concatParentAndCurrent(String parent, String current) {
        if (parent == null || parent.isEmpty()) {
            return current;
        } else if (parent.equals("/") && current.startsWith("/")) {
            return current;
        } else if (current.equals("/")) {
            return parent;
        } else {
            return parent + current;
        }
    }

    public MenuAdapter(ArrayList<Object> objects) {
        for (Object obj : objects) {
            Class<?> objClass = obj.getClass();
            if (!objClass.isAnnotationPresent(Controller.class)) continue;
            String basePath = objClass.getAnnotation(Controller.class).basePath();

            for (Method method : objClass.getDeclaredMethods()) {
                MenuConfig mnConfig = method.getAnnotation(MenuConfig.class);
                FunctionConfig fnConfig = method.getAnnotation(FunctionConfig.class);
                if (mnConfig != null) {
                    String mnKey = concatParentAndCurrent(basePath, mnConfig.path());
                    if (mnMap.containsKey(mnKey)) {
                        throw new RuntimeException("Duplicated MenuKey: " + mnKey + ", in " + objClass.getName() + "." + method.getName());
                    }
                    mnMap.put(mnKey, Pair.of(obj, method));
                }

                if (fnConfig != null) {
                    String fnKey = concatParentAndCurrent(basePath, fnConfig.path());
                    HashMap<Integer, Pair<Object, Method>> pairMap = fnMap.get(fnKey);
                    if (pairMap == null) {
                        HashMap<Integer, Pair<Object, Method>> tmpMap = new HashMap<>();
                        tmpMap.put(fnConfig.key(), Pair.of(obj, method));
                        fnMap.put(fnKey, tmpMap);
                    } else {
                        if (pairMap.containsKey(fnConfig.key()))
                            throw new RuntimeException("Duplicated FunctionKey: " + fnKey + ":" + fnConfig.key() + ", in " + objClass.getName() + "." + method.getName());
                        pairMap.put(fnConfig.key(), Pair.of(obj, method));
                    }
                }
            }
        }
    }

    public Action display(String path, int key, MenuContext context) throws IllegalAccessException, InvocationTargetException {
        HashMap<Integer, Pair<Object, Method>> pairMap = fnMap.get(path);
        if (pairMap == null) throw new RuntimeException("Path does not exist: " + path);
        Pair<Object, Method> pair = pairMap.get(key);
        if (pair == null) throw new RuntimeException("Key does not exist: " + path + " => " + key);
        FunctionConfig fnConfig = pair.getSecond().getAnnotation(FunctionConfig.class);
        if (!fnConfig.divider().isEmpty()) Console.println(fnConfig.divider(), LogEffect.YELLOW);
        Parameter[] parameters = pair.getSecond().getParameters();
        pair.getSecond().setAccessible(true);
        Action returned;
        for (; ; ) {
            try {
                if (parameters.length == 1 && parameters[0].getType().equals(MenuContext.class))
                    returned = (Action) pair.getSecond().invoke(pair.getFirst(), context);
                else
                    returned = (Action) pair.getSecond().invoke(pair.getFirst());
                break;
            } catch (InvocationTargetException e) {
                Throwable cause = e.getCause();
                if (cause instanceof RetryFunctionException)
                    Console.println(cause.getMessage(), LogEffect.RED);
                else
                    throw e;
            }
        }
        return returned;
    }

    public Action display(String path, MenuContext context) throws InvocationTargetException, IllegalAccessException {
        Pair<Object, Method> mnPair = mnMap.get(path);

        if (mnPair != null) {
            MenuConfig mnConfig = mnPair.getSecond().getAnnotation(MenuConfig.class);
            if (!mnConfig.divider().isEmpty()) Console.println(mnConfig.divider(), LogEffect.YELLOW);
            if (mnConfig.exec()) {
                Parameter[] parameters = mnPair.getSecond().getParameters();
                Object returned;
                mnPair.getSecond().setAccessible(true);
                for (; ; ) {
                    try {
                        if (parameters.length == 1 && parameters[0].getType().equals(MenuContext.class))
                            returned = mnPair.getSecond().invoke(mnPair.getFirst(), context);
                        else
                            returned = mnPair.getSecond().invoke(mnPair.getFirst());
                        break;
                    } catch (InvocationTargetException e) {
                        Throwable cause = e.getCause();
                        if (cause instanceof RetryFunctionException)
                            Console.println(cause.getMessage(), LogEffect.RED);
                        else
                            throw e;
                    }
                }
                if (returned instanceof JumpToMenu || returned instanceof JumpToFunction || returned instanceof Exit)
                    return (Action) returned;
            }
        } else
            Console.println(MenuAdapter.divider, LogEffect.YELLOW);

        HashMap<Integer, Pair<Object, Method>> fnPairsMap = fnMap.get(path);
        if (fnPairsMap == null) throw new RuntimeException("Path does not exist: " + path);

        fnPairsMap.keySet().stream().sorted(Integer::compareTo).forEach((key) -> {
            FunctionConfig fnConfig = fnPairsMap.get(key).getSecond().getAnnotation(FunctionConfig.class);
            Console.println(key + ". " + fnConfig.description(), LogEffect.BOLD);
        });

        Integer key = ProjectUtil.<Integer>getLine("请输入你的选择：", (line) -> {
            try {
                int i = Integer.parseInt(line);
                return fnPairsMap.containsKey(i) ? Either.right(i) : Either.left("输入范围错误，请重试！");
            } catch (NumberFormatException e) {
                return Either.left("输入格式错误，请重试！");
            }
        });

        return display(path, key, context);
    }

    public Action display(MenuContext context) throws InvocationTargetException, IllegalAccessException {
        if (context.getCurrentKey() == null) {
            return display(context.getCurrentPath(), context);
        } else {
            return display(context.getCurrentPath(), context.getCurrentKey(), context);
        }
    }

    public static void start(ArrayList<Object> objects, String path) throws InvocationTargetException, IllegalAccessException {
        MenuAdapter menuAdapter = new MenuAdapter(objects);
        MenuContext context = new MenuContext(path, null);

        while (true) {
            Action displayResult = menuAdapter.display(context);

            if (displayResult instanceof Exit) {
                break;
            } else if (displayResult instanceof JumpToMenu) {
                context.setCurrentPath(((JumpToMenu) displayResult).getPath());
                context.setCurrentKey(null);
            } else if (displayResult instanceof JumpToFunction) {
                context.setCurrentPath(((JumpToFunction) displayResult).getPath());
                context.setCurrentKey(((JumpToFunction) displayResult).getKey());
            } else
                throw new RuntimeException("Return value is invalid: " + displayResult);
        }
    }
}
